﻿//
// File: BitmapExtensions.cs
// 
// The BitmapExtensions class defines extensions to the GDI+ Bitmap class that provide
// floodfill operations against bitmaps including support for filling non-opaque regions
// by using alpha blending. This allows coloring of interior regions of shapes drawn with
// anti-aliasing and a transparent background.
//
// These methods are written to be consumed by the SmoothDraw tool and are not as robust
// as they would be if they were intended for general consumption.
//

using System;
using System.Collections.Generic;
using System.Drawing;

namespace GdiPlusExtensions
{
    public static class BitmapExtensions
    {
        public static Rectangle Center (this Bitmap bitmap)
        {
            Rectangle bounds = bitmap.GetOpaqueBounds();

            var newX = (bitmap.Size.Width - bounds.Width) / 2;
            var newY = (bitmap.Size.Height - bounds.Height) / 2;

            if (newX == bounds.X && newY == bounds.Y)
                return bounds;

            using (Bitmap tempPic = new Bitmap (bitmap))
            {
                bitmap.Fill (Color.Transparent);

                for (var x0 = 0; x0 < bounds.Width; ++x0)
                    for (var y0 = 0; y0 < bounds.Height; ++y0)
                    {
                        Color dot = tempPic.GetPixel (bounds.X+x0, bounds.Y+y0);
                        bitmap.SetPixel (newX+x0, newY+y0, dot);
                    }
            }

            return new Rectangle (newX, newY, bounds.Width, bounds.Height);
        }


        public static void FloodFill (this Bitmap bitmap, Color fillColor, int seedX, int seedY)
        {
            Color targetColor = bitmap.GetPixel (seedX, seedY);
            var seed = new Stack<Point>();
            seed.Push (new Point (seedX, seedY));
            Fill (bitmap, seed, targetColor, fillColor);
        }


        private static void Fill (Bitmap bitmap, Stack<Point> seed, Color target, Color fillColor)
        {
            var targetArgb = target.ToArgb();
            while (seed.Count > 0)
            {
                var pixel = seed.Pop();
                if (bitmap.GetPixel (pixel.X, pixel.Y).ToArgb() == targetArgb)
                {
                    bitmap.SetPixel (pixel.X, pixel.Y, fillColor);
                    if (pixel.X > 0 && bitmap.GetPixel (pixel.X-1, pixel.Y).ToArgb() == targetArgb)
                        seed.Push (new Point (pixel.X-1, pixel.Y));
                    if (pixel.Y > 0 && bitmap.GetPixel (pixel.X, pixel.Y-1).ToArgb() == targetArgb)
                        seed.Push (new Point (pixel.X, pixel.Y-1));
                    if (pixel.X+1 < bitmap.Width && bitmap.GetPixel (pixel.X+1, pixel.Y).ToArgb() == targetArgb)
                        seed.Push (new Point (pixel.X+1, pixel.Y));
                    if (pixel.Y+1 < bitmap.Height && bitmap.GetPixel (pixel.X, pixel.Y+1).ToArgb() == targetArgb)
                        seed.Push (new Point (pixel.X, pixel.Y+1));
                }
            }
        }

        public static void Fill (this Bitmap bitmap, Color fillColor)
        {
            for (var x = 0; x < bitmap.Size.Width; ++x)
                for (var y = 0; y < bitmap.Size.Height; ++y)
                    bitmap.SetPixel (x, y, fillColor);
        }

        public static void Fill (this Bitmap bitmap, Color fillColor, int x, int y, int width, int height)
        {
            for (var x0 = 0; x0 < width; ++x0)
                for (var y0 = 0; y0 < height; ++y0)
                    bitmap.SetPixel (x+x0, y+y0, fillColor);
        }

        public static void FillOutside (this Bitmap bitmap, Color fillColor, Rectangle bounds)
        {
            // 111
            // 3 4
            // 222
            bitmap.Fill (fillColor, 0, 0, bitmap.Width, bounds.Y);
            bitmap.Fill (fillColor, 0, bounds.Y+bounds.Height, bitmap.Width, bitmap.Height - (bounds.Y+bounds.Height));
            bitmap.Fill (fillColor, 0, bounds.Y, bounds.X, bounds.Height);
            bitmap.Fill (fillColor, bounds.X+bounds.Width, bounds.Y, bitmap.Width-(bounds.X+bounds.Width), bounds.Height);
        }


        public static void FloodFillAlpha (this Bitmap bitmap, Color fillColor, int seedX, int seedY)
        {
            if (bitmap.GetPixel (seedX, seedY).A != 0)
                return;

            var seed = new Stack<Point>();
            seed.Push (new Point (seedX, seedY));
            AlphaBlend (bitmap, fillColor, seed);
        }


        public static void FloodFillAlphaFromRectangle (this Bitmap bitmap, Color fillColor, Rectangle bounds)
        {
            var seed = new Stack<Point>();

            for (int y0 = 0; y0 < bounds.Height; ++y0)
            {
                if (bitmap.GetPixel (bounds.X, bounds.Y+y0).A != 0xff)
                    seed.Push (new Point (bounds.X, bounds.Y+y0));
                if (bitmap.GetPixel (bounds.X+bounds.Width-1, bounds.Y+y0).A != 0xff)
                    seed.Push (new Point (bounds.X+bounds.Width-1, bounds.Y+y0));
            }

            for (int x0 = 1; x0 < bounds.Width-1; ++x0)
            {
                if (bitmap.GetPixel (bounds.X+x0, bounds.Y).A != 0xff)
                    seed.Push (new Point (bounds.X+x0, bounds.Y));
                if (bitmap.GetPixel (bounds.X+x0, bounds.Y+bounds.Height-1).A != 0xff)
                    seed.Push (new Point (bounds.X+x0, bounds.Y+bounds.Height-1));
            }

            AlphaBlend (bitmap, fillColor, seed);
        }


        // 8-way flood fill.
        private static void AlphaBlend (this Bitmap bitmap, Color fillColor, Stack<Point> seed)
        {
            while (seed.Count > 0)
            {
                var node = seed.Pop();
                var dot = bitmap.GetPixel (node.X, node.Y);
                int alpha = dot.A;
                if (alpha != 0xff)
                {
                    double blend = dot.A / 255.0;
                    int r = (int) (dot.R * blend + fillColor.R * (1.0-blend));
                    int g = (int) (dot.G * blend + fillColor.G * (1.0-blend));
                    int b = (int) (dot.B * blend + fillColor.B * (1.0-blend));

                    bitmap.SetPixel (node.X, node.Y, Color.FromArgb (r, g, b));
                    if (alpha > 0)
                        ++alpha;

                    // 275
                    // 1 4
                    // 386
                    if (node.X > 0)
                    {
                        if (bitmap.GetPixel (node.X-1, node.Y).A >= alpha)
                            seed.Push (new Point (node.X-1, node.Y));
                        if (node.Y > 0 && bitmap.GetPixel (node.X-1, node.Y-1).A >= alpha)
                            seed.Push (new Point (node.X-1, node.Y-1));
                        if (node.Y+1 < bitmap.Height && bitmap.GetPixel (node.X-1, node.Y+1).A >= alpha)
                            seed.Push (new Point (node.X-1, node.Y+1));
                    }
                    if (node.X+1 < bitmap.Width)
                    {
                        if (bitmap.GetPixel (node.X+1, node.Y).A >= alpha)
                            seed.Push (new Point (node.X+1, node.Y));
                        if (node.Y > 0 && bitmap.GetPixel (node.X+1, node.Y-1).A >= alpha)
                            seed.Push (new Point (node.X+1, node.Y-1));
                        if (node.Y+1 < bitmap.Height && bitmap.GetPixel (node.X+1, node.Y+1).A >= alpha)
                            seed.Push (new Point (node.X+1, node.Y+1));
                    }
                    if (node.Y > 0 && bitmap.GetPixel (node.X, node.Y-1).A >= alpha)
                        seed.Push (new Point (node.X, node.Y-1));
                    if (node.Y+1 < bitmap.Height && bitmap.GetPixel (node.X, node.Y+1).A >= alpha)
                        seed.Push (new Point (node.X, node.Y+1));
                }
            }
        }


        public static void Flatten (this Bitmap bitmap, Color bgColor)
        {
            for (var x = 0; x < bitmap.Size.Width; ++x)
                for (var y = 0; y < bitmap.Size.Height; ++y)
                {
                    var dot = bitmap.GetPixel (x, y);
                    if (dot.A < 0xff)
                    {
                        double ratio = dot.A / 255.0;
                        int r = (int) (dot.R * ratio + bgColor.R * (1.0-ratio));
                        int g = (int) (dot.G * ratio + bgColor.G * (1.0-ratio));
                        int b = (int) (dot.B * ratio + bgColor.B * (1.0-ratio));
                        bitmap.SetPixel (x, y, Color.FromArgb (r, g, b));
                    }
                }
        }


        public static void FlattenMiddleground (this Bitmap bitmap, Color mgColor, Rectangle fgBounds)
        {
            if (fgBounds.Width == 0)
                return;

            using (var mask = new Bitmap (bitmap))
            {
                // These 2 colors are arbitrary as long as they are fully opaque.
                mask.FillOutside (Color.Yellow, fgBounds);
                mask.FloodFillAlphaFromRectangle (Color.Magenta, fgBounds);

                for (var x = fgBounds.X; x < fgBounds.X + fgBounds.Width; ++x)
                    for (var y = fgBounds.Y; y < fgBounds.Y + fgBounds.Height; ++y)
                    {
                        var dot = mask.GetPixel (x, y);
                        if (dot.A != 0xff)
                        {
                            double ratio = dot.A / 255.0;
                            int r = (int) (dot.R * ratio + mgColor.R * (1.0-ratio));
                            int g = (int) (dot.G * ratio + mgColor.G * (1.0-ratio));
                            int b = (int) (dot.B * ratio + mgColor.B * (1.0-ratio));

                            bitmap.SetPixel (x, y, Color.FromArgb (r, g, b));
                        }
                    }
            }
        }


        public static Rectangle GetOpaqueBounds (this Bitmap bitmap)
        {
            int edgeXL, edgeXR, edgeYT, edgeYB;

            for (edgeXL = 0; edgeXL < bitmap.Width; ++edgeXL)
                for (var y = 0; y < bitmap.Height; ++y)
                    if (bitmap.GetPixel (edgeXL, y).A != 0)
                        goto L1;
        L1:
            if (edgeXL == bitmap.Width)
                // Image is fully transparent so return empty rectangle.
                return new Rectangle (0, 0, 0, 0);

            for (edgeXR = bitmap.Width-1; edgeXR > edgeXL; --edgeXR)
                for (var y = 0; y < bitmap.Height; ++y)
                    if (bitmap.GetPixel (edgeXR, y).A != 0)
                        goto L2;
        L2:
            for (edgeYT = 0; edgeYT < bitmap.Height; ++edgeYT)
                for (var x = edgeXL+1; x < edgeXR-1; ++x)
                    if (bitmap.GetPixel (x, edgeYT).A != 0)
                        goto L3;
        L3:
            for (edgeYB = bitmap.Height-1; edgeYB > edgeYT; --edgeYB)
                for (var x = edgeXL+1; x < edgeXR-1; ++x)
                    if (bitmap.GetPixel (x, edgeYB).A != 0)
                        goto L4;
        L4:
            return new Rectangle (edgeXL, edgeYT, edgeXR-edgeXL+1, edgeYB-edgeYT+1);
        }
    }
}
